﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Xml;
using System.Xml.Serialization;
using Ensynch.Collections.Generic;
using Microsoft.ResourceManagement.Client;
using Microsoft.ResourceManagement.ObjectModel;
using Microsoft.ResourceManagement.WebServices;

namespace Ensynch.FIM
{
    public class FimTools
    {
        // Create a logger for use in this class.
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(
                System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        public const string ATTRIBUTE_DISPLAY_NAME = "DisplayName";
        public const string ATTRIBUTE_OBJECT_ID = "ObjectID";
        public const string ATTRIBUTE_OBJECT_TYPE = "ObjectType";
        public const string ATTRIBUTE_ACCOUNT_NAME = "AccountName";
        public const string ATTRIBUTE_NAME = "Name";
        public const string ATTRIBUTE_MULTI_VALUED = "Multivalued";
        public const string ATTRIBUTE_DATA_TYPE = "DataType";

        public const string OBJECT_PERSON = "Person";
        public const string OBJECT_RESOURCE = "Resource";

        public const string TYPE_REFERENCE = "Reference";

        public const string XPATH_OBJECTS = "/ObjectTypeDescription";
        public const string XPATH_ATTRIBUTES =
            "/BindingDescription[BoundObjectType = /ObjectTypeDescription[Name = '{0}']]/BoundAttributeType";
        public const string XPATH_OBJECT_ID = "/*[ObjectID = '{0}']";
        public const string XPATH_ATTRIBUTE =
            "/AttributeTypeDescription[Name = '{0}']";

        public const string FORMAT_GUID = "{0}";
        public const string FORMAT_GUID_DISPLAY_NAME = "{0} ({1})";
        public const string FORMAT_DISPLAY_NAME_GUID = "{1} ({0})";
        public const string FORMAT_DISPLAY_NAME = "{1}";

        private static Dictionary<string, string> idMap = 
            new Dictionary<string, string>();

        private static Dictionary<string, bool> isMultiValued =
            new Dictionary<string, bool>();

        private static readonly string registryPath =
            @"HKEY_LOCAL_MACHINE\SOFTWARE\Ensynch\IDA\Workflow\Activities";
        private static readonly string traceLevelRegKey = "TraceLevel";

        private FimWsProxy fimWsProxy;
        public FimWsProxy FimWsProxy
        {
            get
            {
                LogTrace(false);

                return fimWsProxy;
            }
        }

        private static FimTools defaultInstance;
        public static FimTools DefaultInstance
        {
            get
            {
                if (defaultInstance == null)
                {
                    defaultInstance = new FimTools();
                }
                return defaultInstance;
            }
        }

        private FimTools()
        {
            fimWsProxy = FimWsProxy.DefaultInstance;
        }

        public FimTools(DefaultClient client)
        {
            fimWsProxy = new FimWsProxy(client);
        }

        public FimTools(DefaultClient client, bool enableCache)
        {
            fimWsProxy = new FimWsProxy(client);
            cacheEnabled = enableCache;
        }

        #region Object/Attribute dictionaries

        // Cache is disabled in the default instance.
        private bool cacheEnabled = false;

        public const string REG_PATH = @"HKEY_CURRENT_USER\SOFTWARE\Ensynch.FIM.FimTools\1.0.0.0";

        public const string REG_KEY_OBJECT_DICTIONARY = "ObjectDictionary";
        public const string REG_KEY_ATTRIBUTE_DICTIONARY = "AttributeDictionary";
        public const string REG_KEY_ATTRIBUTE_TYPE_DICTIONARY = "AttributeTypeDictionary";

        private SerializableDictionary<string, string> objectDictionary;
        public SerializableDictionary<string, string> ObjectDictionary
        {
            get
            {
                if (objectDictionary == null)
                {
                    if (!tryGetObjectDictionaryFromRegistry())
                    {
                        RefreshObjectDictionary();
                    }
                }
                return objectDictionary;
            }
        }

        private bool tryGetObjectDictionaryFromRegistry()
        {
            if (!cacheEnabled)
            {
                return false;
            }

            try
            {
                object compressed = EnsynchTools.GetRegValue(REG_PATH, REG_KEY_OBJECT_DICTIONARY);
                if (compressed != null)
                {
                    string uncompressed = EnsynchTools.Uncompress(compressed.ToString());
                    using (StringReader stringReader = new StringReader(uncompressed))
                    using (XmlTextReader xmlTextReader = new XmlTextReader(stringReader))
                    {
                        XmlSerializer xmlSerializer =
                            new XmlSerializer(typeof(SerializableDictionary<string, string>));
                        objectDictionary = (SerializableDictionary<string, string>)
                            xmlSerializer.Deserialize(xmlTextReader);
                    }
                    return true;
                }
            }
            catch (Exception exc)
            {
                log.Error("[tryGetObjectDictionaryFromRegistry]", exc);
            }

            return false;
        }

        public void RefreshObjectDictionary()
        {
            objectDictionary = new SerializableDictionary<string, string>();

            List<string> selection = new List<string>
                    {
                      FimTools.ATTRIBUTE_DISPLAY_NAME,
                      FimTools.ATTRIBUTE_NAME
                    };
            List<string> sorting = null;

            IEnumerable<RmResource> objects =
                this.FimWsProxy.DefaultClient.Enumerate(FimTools.XPATH_OBJECTS, selection.ToArray());
            foreach (RmResource obj in objects)
            {
                objectDictionary.Add(
                    obj[FimTools.ATTRIBUTE_NAME].Value.ToString(),
                    obj[FimTools.ATTRIBUTE_DISPLAY_NAME].Value.ToString());
            }

            cacheDictionary(objectDictionary, REG_KEY_OBJECT_DICTIONARY);
        }

        private SerializableDictionary<string, SerializableDictionary<string, string>> attributeMap;
        private SerializableDictionary<string, SerializableDictionary<string, string>> AttributeMap
        {
            get
            {
                if (attributeMap == null)
                {
                    attributeMap = new SerializableDictionary<string, SerializableDictionary<string, string>>();
                }
                return attributeMap;
            }
        }

        private SerializableDictionary<string, SerializableDictionary<string, RmFactory.RmAttributeType>> attributeTypeMap;
        private SerializableDictionary<string, SerializableDictionary<string, RmFactory.RmAttributeType>> AttributeTypeMap
        {
            get
            {
                if (attributeTypeMap == null)
                {
                    attributeTypeMap = new SerializableDictionary<string, SerializableDictionary<string, RmFactory.RmAttributeType>>();
                }
                return attributeTypeMap;
            }
        }

        public SerializableDictionary<string, string> GetAttributeDictionary(string objectName)
        {
            checkAttributeDictionary(objectName);
            return AttributeMap[objectName];
        }

        public SerializableDictionary<string, RmFactory.RmAttributeType> GetAttributeTypeDictionary(string objectName)
        {
            checkAttributeDictionary(objectName);
            return AttributeTypeMap[objectName];
        }

        private void checkAttributeDictionary(string objectName)
        {
            if (!AttributeMap.ContainsKey(objectName))
            {
                if (!tryGetAttributeDictionaryFromRegistry(objectName, REG_KEY_ATTRIBUTE_DICTIONARY) ||
                    !tryGetAttributeDictionaryFromRegistry(objectName, REG_KEY_ATTRIBUTE_TYPE_DICTIONARY))
                {
                    RefreshAttributeDictionary(objectName);
                }
            }
        }

        private bool tryGetAttributeDictionaryFromRegistry(string objectName, string dictionaryName)
        {
            if (!cacheEnabled)
            {
                return false;
            }

            try
            {
                object compressed = EnsynchTools.GetRegValue(REG_PATH, objectName + dictionaryName);
                if (compressed != null)
                {
                    string uncompressed = EnsynchTools.Uncompress(compressed.ToString());
                    using (StringReader stringReader = new StringReader(uncompressed))
                    using (XmlTextReader xmlTextReader = new XmlTextReader(stringReader))
                    {
                        XmlSerializer xmlSerializer;
                        switch (dictionaryName)
                        {
                            case REG_KEY_ATTRIBUTE_DICTIONARY:
                                xmlSerializer =
                                    new XmlSerializer(typeof(SerializableDictionary<string, string>));
                                SerializableDictionary<string, string> attrDict = 
                                    (SerializableDictionary<string, string>)xmlSerializer.Deserialize(xmlTextReader);
                                AttributeMap.Add(objectName, attrDict);
                                break;
                            case REG_KEY_ATTRIBUTE_TYPE_DICTIONARY:
                                xmlSerializer =
                                    new XmlSerializer(typeof(SerializableDictionary<string, RmFactory.RmAttributeType>));
                                SerializableDictionary<string, RmFactory.RmAttributeType> attrTypeDict =
                                    (SerializableDictionary<string, RmFactory.RmAttributeType>)xmlSerializer.Deserialize(xmlTextReader);
                                AttributeTypeMap.Add(objectName, attrTypeDict);
                                break;
                            default:
                                throw new Exception("Unexpected dictionary name: " + dictionaryName);
                                break;
                        }
                    }
                    return true;
                }
            }
            catch (Exception exc)
            {
                log.Error("[tryGetAttributeDictionaryFromRegistry]", exc);
            }

            return false;
        }

        public void RefreshAttributeDictionary(string objectName)
        {
            SerializableDictionary<string, string> attrDict = new SerializableDictionary<string, string>();
            SerializableDictionary<string, RmFactory.RmAttributeType> attrTypeDict =
                new SerializableDictionary<string, RmFactory.RmAttributeType>();

            List<string> selection = new List<string>
                    {
                      FimTools.ATTRIBUTE_DISPLAY_NAME,
                      FimTools.ATTRIBUTE_NAME,
                      FimTools.ATTRIBUTE_DATA_TYPE
                    };
            List<string> sorting = null;

            string xpath = string.Format(FimTools.XPATH_ATTRIBUTES, objectName);

            IEnumerable<RmResource> objects =
                this.FimWsProxy.DefaultClient.Enumerate(xpath, selection.ToArray());
            foreach (RmResource obj in objects)
            {
                string name = obj[FimTools.ATTRIBUTE_NAME].Value.ToString();
                string displayName = obj[FimTools.ATTRIBUTE_DISPLAY_NAME].Value.ToString();
                string typeString = obj[FimTools.ATTRIBUTE_DATA_TYPE].Value.ToString();
                RmFactory.RmAttributeType type;
                attrDict.Add(name, displayName);
                if (Enum.IsDefined(typeof(RmFactory.RmAttributeType), typeString))
                {
                    type = (RmFactory.RmAttributeType)Enum.Parse(typeof(RmFactory.RmAttributeType), typeString, true);
                }
                else
                {
                    type = RmFactory.RmAttributeType.String;
                }
                attrTypeDict.Add(name,type);
            }

            if (!AttributeMap.ContainsKey(objectName))
            {
                AttributeMap.Add(objectName, null);
            }
            AttributeMap[objectName] = attrDict;

            if (!AttributeTypeMap.ContainsKey(objectName))
            {
                AttributeTypeMap.Add(objectName, null);
            }
            AttributeTypeMap[objectName] = attrTypeDict;

            cacheDictionary(attrDict, objectName + REG_KEY_ATTRIBUTE_DICTIONARY);
            cacheDictionary(attrTypeDict, objectName + REG_KEY_ATTRIBUTE_TYPE_DICTIONARY);
        }

        private void cacheDictionary(object attrDict, string regKeyName)
        {
            if (!cacheEnabled)
            {
                return;
            }

            using (StringWriter stringWriter = new StringWriter())
            using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter))
            {
                XmlSerializer xmlSerializer =
                    new XmlSerializer(attrDict.GetType());
                xmlSerializer.Serialize(xmlTextWriter, attrDict);

                stringWriter.Flush();
                string uncompressed = stringWriter.ToString();
                string compressed = EnsynchTools.Compress(uncompressed);
                EnsynchTools.SetRegValue(REG_PATH, regKeyName, compressed);
            }
        }

        #endregion

        /// <summary>
        /// Deprecated. Use log4net.Core.Level instead.
        /// Trace Level     Value
        /// Off             0
        /// Error           1
        /// Warning         2
        /// Info            3
        /// Verbose         4 
        /// </summary>
        public static TraceLevel TheTraceLevel
        {
            get
            {
                object regObj = EnsynchTools.GetRegValue(registryPath, traceLevelRegKey);
                if (regObj != null)
                {
                    int test;
                    if (int.TryParse(regObj.ToString(), out test))
                    {
                        if (test >= (int)TraceLevel.Off && test <= (int)TraceLevel.Verbose)
                        {
                            return (TraceLevel)test;
                        }
                    }
                }
                return TraceLevel.Warning;
            }
        }

        public static void SplitXPathFilter(string filter, out string prefix,
            out string booleanExpression, out string suffix)
        {
            prefix = booleanExpression = suffix = null;
            string pattern = @"(.*?)\[(.*)\](.*?)";
            List<string> tokens = EnsynchTools.RegexCaptures(pattern, filter);

            if (tokens.Count > 0)
            {
                prefix = tokens[0];
                if (tokens.Count > 1)
                {
                    booleanExpression = tokens[1];
                    if (tokens.Count > 2)
                    {
                        suffix = tokens[2];
                    }
                }
            }
        }

        public static List<object> QueryRequestParameters(List<string> requestParameters, string xpath)
        {
            List<object> result = new List<object>();

            StringBuilder buffer = new StringBuilder();
            buffer.AppendLine("<RequestParameters>");
            foreach (string requestParam in requestParameters)
            {
                buffer.AppendLine(requestParam);
            }
            buffer.AppendLine("</RequestParameters>");

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(buffer.ToString());

            XmlNodeList nodes = doc.SelectNodes(xpath);
            foreach (XmlNode node in nodes)
            {
                object value;
                if (tryParseXmlNodeAsType(node, out value))
                {
                    result.Add(value);
                }
                else
                {
                    result.Add(node.InnerText);
                }
            }

            if (true) //(log.IsDebugEnabled)
            {
                buffer = new StringBuilder();
                StringWriter sw = new StringWriter();
                XmlTextWriter xtw = new XmlTextWriter(sw);
                xtw.Formatting = Formatting.Indented;
                doc.WriteTo(xtw);
                string xml = sw.ToString();
                buffer.AppendFormat(
@"[QueryRequestParameters] XML:
{0}

[QueryRequestParameters] XPath:
{1}

[QueryRequestParameters] Result:
{2}",
                    xml,
                    xpath,
                    PrintObjectList(result, true));
                log.Debug(buffer.ToString());
            }

            return result;
        }

        public static string PrintObjectList(List<object> objects, bool forTraceLog)
        {
            Type homogeneousType;
            if (isHomogeneousType(objects, out homogeneousType))
            {
                if (homogeneousType == typeof(Guid))
                {
                    return ReferencesToString(objects);
                }
            }
            StringBuilder buffer = new StringBuilder();
            foreach (object obj in objects)
            {
                AppendObject(buffer, obj, forTraceLog);
            }
            return buffer.ToString();
        }

        public static void AppendObject(StringBuilder buffer, object obj, bool forTraceLog)
        {
            if (forTraceLog)
            {
                if (obj == null)
                {
                    AppendObject(buffer, "<NULL>");
                }
                else
                {
                    AppendObject(buffer, string.Format("{0} ({1})", obj.ToString(), obj.GetType()));
                }
            }
            else
            {
                if (obj != null)
                {
                    AppendObject(buffer, obj.ToString());
                }
            }
        }

        public static void AppendObject(StringBuilder buffer, string str)
        {
            if (buffer.Length > 0)
            {
                buffer.AppendLine();
            }
            buffer.Append(str);
        }

        private static bool isHomogeneousType(List<object> objects, out Type homogeneousType)
        {
            Type test = null;
            homogeneousType = null;
            foreach (object obj in objects)
            {
                if (test == null)
                {
                    test = obj.GetType();
                    continue;
                }
                if (obj.GetType() != test)
                {
                    return false;
                }
            }
            homogeneousType = test;
            return true;
        }

        private static bool tryParseXmlNodeAsType(XmlNode node, out object value)
        {
            value = null;

            if (node == null)
            {
                return false;
            }

            StringBuilder buffer = new StringBuilder();
            buffer.AppendLine("Examining attributes on:\r\n" + node.OuterXml);
            foreach (XmlAttribute attribute in node.Attributes)
            {
                buffer.AppendFormat("{0} = {1}\r\n", attribute.Name, attribute.Value);
            }
            log.Debug(buffer.ToString());

            string typeAttributeName;
            if (tryGetXmlNodeTypeAttributeName(node, out typeAttributeName))
            {
                string typeAttributeValue = node.Attributes[typeAttributeName].Value;
                string msg = string.Format("Found {0}='{1}' for XML node:\r\n{2}",
                    typeAttributeName, typeAttributeValue, node.OuterXml);
                log.Debug(msg);
                string[] tokens = typeAttributeValue.Split(':');
                string nodeType = tokens.Last().ToLower();
                switch (nodeType)
                {
                    case "string":
                        value = node.InnerText;
                        return true;
                        break;
                    case "datetime":
                        DateTime dt;
                        bool dtSuccess = DateTime.TryParse(node.InnerText, out dt);
                        value = dt;
                        return dtSuccess;
                        break;
                    case "boolean":
                        bool b;
                        bool bSuccess = Boolean.TryParse(node.InnerText, out b);
                        value = b;
                        return bSuccess;
                        break;
                    case "guid":
                        bool gSuccess = false;
                        try
                        {
                            Guid g = new Guid(node.InnerText);
                            value = g;
                            gSuccess = true;
                        }
                        catch { }
                        return gSuccess;
                        break;
                }
            }
            else
            {
                string msg = string.Format("Could not find type for XML node:\r\n{0}",
                    node.OuterXml);
                log.Debug(msg);
            }

            return false;
        }

        private static bool tryGetXmlNodeTypeAttributeName(XmlNode node, out string attributeName)
        {
            attributeName = null;
            foreach (XmlAttribute attribute in node.Attributes)
            {
                string[] attributeTokens = attribute.Name.Split(':');
                if (attributeTokens.Last().ToLower() == "type")
                {
                    attributeName = attribute.Name;
                    return true;
                }
            }
            return false;
        }

        public static string ResolveRequestParameterQueries(string input, ref List<string> requestParameters)
        {
            int index = input.IndexOf("[//RequestParameter");
            if (index < 0)
            {
                return input;
            }
            if (requestParameters == null)
            {
                return input;
            }

            string query;
            if (!FimTools.ExtractRequestParameterQuery(input, out query))
            {
                return input;
            }

            string innerQuery = query.Substring(1, query.Length - 2);
            List<object> queryResults = QueryRequestParameters(requestParameters, innerQuery);
            string queryResult = PrintObjectList(queryResults, false); 
            string output = input.Replace(query, queryResult);

            string msg = string.Format(
@"[ResolveRequestParameterQueries] iteration:
Input:
{0}

Output:
{1}",
                input, output);
            log.Debug(msg);

            return ResolveRequestParameterQueries(output, ref requestParameters);
        }

        public static bool ExtractRequestParameterQuery(string input, out string query)
        {
            string failureMsg = string.Format(
@"Could not extract request parameter query from input:
{0}",
                input);

            query = null;
            int start = input.IndexOf("[//RequestParameter");
            if (start < 0)
            {
                log.Debug(failureMsg);
                return false;
            }
            int bracketCount = 0;
            int i = start;
            do
            {
                switch (input[i])
                {
                    case '[':
                        bracketCount++;
                        break;
                    case ']':
                        bracketCount--;
                        break;
                }
                i++;
            } while (bracketCount > 0 && i < input.Length);
            if (bracketCount > 0)
            {
                log.Debug(failureMsg);
                return false;
            }
            query = input.Substring(start, i - start);
            string msg = string.Format(
@"Successfully extracted request parameter query from input:
{0}

Extracted query:
{1}",
                input, query);
            log.Debug(msg);
            return true;
        }

        public static bool ExtractAllXPathGrammar(string input, ref List<string> grammar)
        {
            if (string.IsNullOrEmpty(input))
            {
                return true;
            }

            string failureMsg = string.Format(
@"Syntax error parsing XPath grammar from input:
{0}",
                input);

            // Work backwards so that we can use LastIndexOf to divide and conquer.
            int start = input.LastIndexOf("[//");
            if (start < 0)
            {
                return true;
            }
            int bracketCount = 0;
            int i = start;
            do
            {
                switch (input[i])
                {
                    case '[':
                        bracketCount++;
                        break;
                    case ']':
                        bracketCount--;
                        break;
                }
                i++;
            } while (bracketCount > 0 && i < input.Length);
            if (bracketCount > 0)
            {
                log.Error(failureMsg);
                return false;
            }
            string query = input.Substring(start, i - start);
            grammar.Insert(0, query);
            string msg = string.Format(
@"[ExtractAllXPathGrammar] iteration:
Successfully extracted XPath grammar from input:
{0}

Extracted grammar:
{1}",
                input, query);
            log.Debug(msg);
            return ExtractAllXPathGrammar(input.Substring(0, start), ref grammar);
        }

        public string PrintResourceProperty(object obj, bool forTraceLog)
        {
            try
            {
                if (obj.GetType() == typeof(List<UniqueIdentifier>))
                {
                    List<UniqueIdentifier> idList =
                        (List<UniqueIdentifier>)obj;
                    string result;
                    if (forTraceLog)
                    {
                        result = DereferenceIDsToString(idList);
                    }
                    else
                    {
                        result = ReferencesToString(idList.ToArray());
                    }
                    return result;
                }
            }
            catch { }
            try
            {
                if (obj.GetType() == typeof(UniqueIdentifier))
                {
                    string result;
                    if (forTraceLog)
                    {
                        result = DereferenceIDsToString((UniqueIdentifier)obj);
                    }
                    else
                    {
                        result = ((UniqueIdentifier)obj).Value;
                    }
                    return result;
                }
            }
            catch { }
            try
            {
                if (obj.GetType() == typeof(List<string>))
                {
                    StringBuilder result = new StringBuilder();
                    List<string> stringList = (List<string>)obj;
                    foreach (string str in stringList)
                    {
                        if (!result.ToString().Equals(""))
                        {
                            result.AppendLine();
                        }
                        result.Append(str);
                    }
                    return result.ToString();
                }
            }
            catch { }

            return obj.ToString();
        }

        public static string[] TokenizeReferences(string input)
        {
            string[] tokens = new string[0];
            if (input != null)
            {
                char[] delimiters = new char[] { ' ', ';' };
                tokens = input.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
            }
            return tokens;
        }

        public static void AddReferenceToList(string reference, ref StringBuilder list)
        {
            if (!reference.Equals(""))
            {
                if (!list.ToString().Equals(""))
                {
                    list.Append(" ;");
                }
                list.Append(reference);
            }
        }

        public static void AddReferenceToList(UniqueIdentifier reference, ref StringBuilder list)
        {
            AddReferenceToList(reference.GetGuid().ToString(null), ref list);
        }

        public static void AddReferenceToList(Guid reference, ref StringBuilder list)
        {
            AddReferenceToList(reference.ToString(null), ref list);
        }

        public static void AddReferenceToList(string reference, ref object list)
        {
            StringBuilder references = new StringBuilder(list.ToString());
            AddReferenceToList(reference, ref references);
            list = references.ToString();
        }

        public static void AddReferenceToList(string reference, ref string list)
        {
            StringBuilder references = new StringBuilder(list);
            AddReferenceToList(reference, ref references);
            list = references.ToString();
        }

        public static string ReferencesToString(string[] references)
        {
            StringBuilder result = new StringBuilder();
            foreach (string reference in references)
            {
                AddReferenceToList(reference, ref result);
            }
            return result.ToString();
        }

        public static string ReferencesToString(UniqueIdentifier[] references)
        {
            StringBuilder result = new StringBuilder();
            foreach (UniqueIdentifier reference in references)
            {
                AddReferenceToList(reference, ref result);
            }
            return result.ToString();
        }

        public static List<UniqueIdentifier> ReferencesToUniqueIdentifier(string references)
        {
            List<UniqueIdentifier> result = new List<UniqueIdentifier>();
            foreach (string reference in TokenizeReferences(references))
            {
                result.Add(new UniqueIdentifier(reference));
            }
            return result;
        }

        public static string ReferencesToString(List<object> references)
        {
            StringBuilder result = new StringBuilder();
            foreach (object obj in references)
            {
                if (obj.GetType() == typeof(Guid))
                {
                    AddReferenceToList((Guid)obj, ref result);
                }
            }
            return result.ToString();
        }

        public static string ReferencesToString(Guid[] references)
        {
            StringBuilder result = new StringBuilder();
            foreach (Guid reference in references)
            {
                AddReferenceToList(reference, ref result);
            }
            return result.ToString();
        }

        public string DereferenceIDsToString(string references)
        {
            // Quit if empty.
            if (string.IsNullOrEmpty(references))
            {
                return references;
            }

            // Tokenize into an array of object IDs.
            string[] objectIDs = TokenizeReferences(references);

            string dereferences;
            if (TryDereferenceIDsToString(objectIDs.ToList(), out dereferences))
            {
                return dereferences;
            }
            else
            {
                return references;
            }
        }

        public bool TryDereferenceIDsToString(List<string> objectIDs, out string result)
        {
            result = "";

            // Quit if the array is zero length.
            if (objectIDs.Count < 1)
            {
                return false;
            }

            Guid test;
            List<Guid> ids = new List<Guid>();
            for (int i = 0; i < objectIDs.Count; i++)
            {
                // Strip leading "urn:uuid:" string.
                objectIDs[i] = objectIDs[i].Replace("urn:uuid:", "");

                // Quit if the objects fail to parse.
                if (!EnsynchTools.TryParseGuid(objectIDs[i], out test))
                {
                    return false;
                }
                else
                {
                    ids.Add(test);
                }
            }

            result = DereferenceIDsToString(ids);
            return true;
        }

        public string DereferenceIDsToString(UniqueIdentifier objectID)
        {
            List<Guid> idList = new List<Guid>();
            idList.Add(objectID.GetGuid());
            return DereferenceIDsToString(idList);
        }

        public string DereferenceIDsToString(Guid objectID)
        {
            List<Guid> idList = new List<Guid>();
            idList.Add(objectID);
            return DereferenceIDsToString(idList);
        }

        public string DereferenceIDsToString(List<UniqueIdentifier> objectIDs)
        {
            List<Guid> idList = new List<Guid>();
            foreach (UniqueIdentifier id in objectIDs)
            {
                idList.Add(id.GetGuid());
            }
            return DereferenceIDsToString(idList);
        }

        //TODO: rename this
        public string DereferenceIDsToString(List<Guid> objectIDs)
        {
            StringBuilder buffer = new StringBuilder();

            // Finally, resolve the objects.
            Dictionary<string, string> resolvedDictionary = new Dictionary<string, string>();

            foreach (Guid guid in objectIDs)
            {
                string id = guid.ToString();
                if (!resolvedDictionary.ContainsKey(id))
                {
                    string name = GetDisplayName(id);
                    resolvedDictionary.Add(id, name);
                }
            }

            // Add the display names to the ID strings.
            for (int i = 0; i < objectIDs.Count; i++)
            {
                string objectID = objectIDs[i].ToString(null);
                buffer.Append(objectID);
                #region Disable suspect code
                if (resolvedDictionary.ContainsKey(objectID))
                {
                    buffer.Append(" (" + resolvedDictionary[objectID] + ")");
                }
                #endregion
                if (i < objectIDs.Count - 1)
                {
                    buffer.AppendLine();
                }
            }

            return buffer.ToString();
        }

        /// <summary>
        /// Resolve an XPath filter into a dictionary of ObjectID, DisplayName pairs.
        /// </summary>
        /// <param name="xpath">XPath filter.</param>
        /// <param name="sort">If true, sorts by DisplayName.</param>
        /// <returns></returns>
        public static Dictionary<string, string> ResolveObjectIDs(string xpath, bool sort)
        {
            Dictionary<string, string> result = new Dictionary<string, string>();

            List<string> selection = new List<string>
            {
              ATTRIBUTE_DISPLAY_NAME,
              ATTRIBUTE_OBJECT_ID
            };
            List<string> sorting = null;
            if (sort)
            {
                sorting = new List<string>
                {
                    ATTRIBUTE_DISPLAY_NAME
                };
            }

            //TODO: refactor or remove

            //List<ResourceManagementObject> rmObjects =
            //    WSEnumerationClient.GetObjects(xpath, selection, sorting);
            //foreach (ResourceManagementObject obj in rmObjects)
            //{
            //    string objectID = obj[ATTRIBUTE_OBJECT_ID].AttributeSingleValue.ToString()
            //            .ToLower().Replace("urn:uuid:", "");
            //    if (!result.ContainsKey(objectID))
            //    {
            //        result.Add(
            //            objectID,
            //            obj[ATTRIBUTE_DISPLAY_NAME].AttributeSingleValue.ToString());
            //    }
            //}

            return result;
        }

        public string GetDisplayName(string reference)
        {
            string guid = FimTools.NormalizeReference(reference);
            lock (typeof(FimTools))
            {
                if (!idMap.ContainsKey(guid))
                {
                    string displayName = "[Object not found]";
                    StringBuilder buffer = new StringBuilder(
                        "Dereferencing " + guid);
                    try
                    {
                        RmResource obj =
                            FimWsProxy.GetObject(guid, false);
                        if (obj != null)
                        {
                            displayName = "[No display name]";
                            RmAttributeValue attributeValue = null;
                            if (obj.Attributes.ContainsKey(
                                new RmAttributeName(ATTRIBUTE_DISPLAY_NAME)))
                            {
                                attributeValue = obj[ATTRIBUTE_DISPLAY_NAME];
                            }
                            if (attributeValue != null &&
                                attributeValue.Value != null)
                            {
                                string result = attributeValue.Value.ToString();
                                if (!string.IsNullOrEmpty(result))
                                {
                                    displayName = result;
                                }
                            }
                        }
                        buffer.Append(" = " + displayName);
                    }
                    catch
                    {
                        throw;
                    }
                    finally
                    {
                        log.Debug(buffer.ToString());
                    }
                    idMap.Add(guid, displayName);
                }
            }
            return idMap[guid];
        }

        public static string NormalizeReference(string reference)
        {
            string result = null;
            if (reference != null)
            {
                result = reference.ToLower().Replace("urn:uuid:", "");
            }
            return result;
        }

        /// <summary>
        /// Prints references in the specified format.
        /// {0} = ObjectID
        /// {1} = DisplayName
        /// </summary>
        public string PrintReference(string reference, string format)
        {
            string displayName = null;

            if (MustDereferenceGuids(format))
            {
                displayName = GetDisplayName(reference);
            }

            return string.Format(format, reference, displayName);
        }

        /// <summary>
        /// Given a format, determines whether Guids must be dereferenced.
        /// Refer to the following built-in formats as guides:
        /// 
        /// public const string FORMAT_GUID = "{0}";
        /// public const string FORMAT_GUID_DISPLAY_NAME = "{0} ({1})";
        /// public const string FORMAT_DISPLAY_NAME_GUID = "{1} ({0})";
        /// public const string FORMAT_DISPLAY_NAME = "{1}";
        /// </summary>
        public static bool MustDereferenceGuids(string format)
        {
            return format.IndexOf("{1}") >= 0;
        }

        public static void ParseRequestParameters(
            IList<string> requestParameters,
            ref List<string> columnHeaders,
            ref List<Dictionary<string, string>> columnData)
        {
            bool firstDefined = false;
            bool lastDefined = false;
            foreach (string requestParameterXml in requestParameters)
            {
                Dictionary<string, string> rowData =
                    new Dictionary<string, string>();
                columnData.Add(rowData);
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(requestParameterXml);
                XmlNode root = xmlDoc.DocumentElement;
                foreach (XmlNode node in root.ChildNodes)
                {
                    if (!columnHeaders.Contains(node.Name))
                    {
                        // Try to add the column headers in some kind of order.
                        if (node.Name == "PropertyName")
                        {
                            columnHeaders.Insert(0, node.Name);
                            firstDefined = true;
                        }
                        else if (node.Name == "Value")
                        {
                            columnHeaders.Add(node.Name);
                            lastDefined = true;
                        }
                        else if (lastDefined)
                        {
                            columnHeaders.Insert(columnHeaders.Count - 1, node.Name);
                        }
                        else if (firstDefined)
                        {
                            columnHeaders.Insert(1, node.Name);
                        }
                        else
                        {
                            columnHeaders.Add(node.Name);
                        }
                    }
                    rowData.Add(node.Name, node.InnerText);
                }
            }
        }

        public bool IsMultiValued(string attributeName)
        {
            lock (typeof(FimTools))
            {
                if (!isMultiValued.ContainsKey(attributeName))
                {
                    string xpath = string.Format(XPATH_ATTRIBUTE, attributeName);
                    bool? multiValued = null;
                    StringBuilder buffer = new StringBuilder();
                    buffer.AppendLine("Checking if " + attributeName + " is multi-valued.");
                    buffer.AppendLine("Running query: " + xpath);
                    try
                    {
                        List<string> attrList = new List<string>()
                        {
                            ATTRIBUTE_NAME,
                            ATTRIBUTE_MULTI_VALUED
                        };
                        IEnumerable<RmResource> objs =
                            FimWsProxy.GetObjects(xpath, attrList);
                        if (objs != null)
                        {
                            /// Results can only be enumerated once, and using
                            /// First() will throw a generic exception if there
                            /// are no results, "Sequence contains no elements."
                            foreach (RmResource obj in objs)
                            {
                                multiValued = (bool)(obj[ATTRIBUTE_MULTI_VALUED].Value);
                                buffer.AppendLine("Result: " + multiValued);
                            }
                        }
                        if (multiValued == null)
                        {
                            throw new InvalidOperationException(
                                "No such attribute: " + attributeName);
                        }
                    }
                    catch
                    {
                        throw;
                    }
                    finally
                    {
                        log.Debug(buffer.ToString());
                    }
                    isMultiValued.Add(attributeName, multiValued.Value);
                }
            }
            return isMultiValued[attributeName];
        }

        public static string HtmlDecode(string _encoded)
        {
            log.Debug("HTML-decoding '_encoded' expression: " + _encoded);
            string _decoded = HttpUtility.HtmlDecode(_encoded);
            log.Debug("HTML-decoded expression: " + _encoded);
            return _decoded;
        }

        public static string HtmlEncode(string _decoded)
        {
            log.Debug("HTML-encoding '_decoded' expression: " + _decoded);
            string _encoded = HttpUtility.HtmlDecode(_decoded);
            log.Debug("HTML-encoded expression: " + _decoded);
            return _encoded;
        }

        public static string AddSourceHeader(string msg, string instanceName,
            params KeyValuePair<string, string>[] parameters)
        {
            StringBuilder buffer = new StringBuilder();
            foreach (KeyValuePair<string, string> param in parameters)
            {
                buffer.AppendFormat("\r\n{0}: {1}",
                    param.Key.PadRight(16), param.Value);
            }
            return EnsynchTools.AddSourceHeader(msg,
                instanceName +
                buffer.ToString());
        }

        /// <summary>
        /// Do not use in production.
        /// </summary>
        public static void LogTrace(bool includeCallStack)
        {
            #if TRACE
            MethodBase srcMethod = Ensynch.EnsynchTools.WhichMethodCalledMe();
            StringBuilder msg = new StringBuilder(srcMethod.ReflectedType.FullName + "." + srcMethod.Name);
            if (includeCallStack)
            {
                msg.Append("\r\n\r\n" + new StackTrace().ToString());
            }
            log.Debug(msg.ToString());
            #endif
        }

        /// <summary>
        /// Do not use in production.
        /// </summary>
        public static void LogCallStack()
        {
            log.Debug(new StackTrace().ToString());
        }

        public static void LogCurrentUser()
        {
            WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
            log.DebugFormat(
@"[{0}] Current user info:
Name               : {1}
IsAuthenticated    : {2}
AuthenticationType : {3}
ImpersonationLevel : {4}
SID                : {5}",
                EnsynchTools.WhichMethodCalledMe().Name,
                currentUser.Name,
                currentUser.IsAuthenticated,
                currentUser.AuthenticationType,
                currentUser.ImpersonationLevel,
                currentUser.User);
        }

        #region Events

        //public static event EventHandler<LogEventArgs> LogMessageGenerated;

        //protected static void OnLogMessageGenerated(string msg, Level logLevel)
        //{
        //    if (LogMessageGenerated != null)
        //    {
        //        LogMessageGenerated(typeof(FimTools), 
        //            new LogEventArgs(msg, logLevel));
        //    }
        //}

        #endregion
    }
}
